package com.javakc.platform.configurations.security;


import com.fasterxml.jackson.databind.ObjectMapper;
import com.javakc.platform.components.redis.RedisComponent;
import com.javakc.platform.components.result.Result;
import com.javakc.platform.components.result.code.ResultCode;
import com.javakc.platform.components.result.data.ResultData;
import com.javakc.platform.modules.system.user.entity.User;
import com.javakc.platform.modules.system.user.service.UserService;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;


/**
 * 自定义SpringSecurity内存
 */
@Configuration//配置文件
@EnableWebSecurity//启用安全框架
//细粒度  前端按钮对应后端接口      （在方法调用前拦截）
//启用全局方法的安全注解
@EnableGlobalMethodSecurity(prePostEnabled = true)//调用方法时进行拦截校验 没有权限不能调用
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
    //注入用户逻辑层实现类
    @Autowired
    private UserService userService;

    //    注入Jackson 对象  把java对象转json字符串 和把json字符串转java对象
    @Autowired
    private ObjectMapper objectMapper;

    //注入Redis操作工具类组件
    @Autowired
    private RedisComponent redisComponent;

    //定义一个全局常量 :    用户锁定异常
    private final static String USER_LOCKED = "user_locked";
    // 定义一个全局常量 :   用户未找到异常
    private final static String USER_NOT_FOUND = "user_not_found";

    /**
     * 创建密码加密器
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    /**
     * 重写SpringSecurity登陆认证实现             认证管理器的构造器为了构建认证管理器
     *
     * @param auth AuthenticationManagerBuilder  构造器 —>AuthenticationManager
     * @throws Exception 抛出认证异常
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {


        //        1、在内存中默认模拟账号
        //        模拟账号  user
       //        模拟密码

//        2、根据登陆账号查询用户对象
//        通过认证器的管理器构造   (该接口实现类)
//  interface  UserDetailsService ->method loadUserByUsername


//        auth.userDetailsService();
//      认证管理构建器
        auth.authenticationProvider(daoAuthenticationProvider());

    }

    /**
     * 重写Spring Security 数据层底层实现
     *
     * @return DaoAuthenticationProvider
     */
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();

         //        System.out.println(passwordEncoder().encode("123456"));
         //        System.out.println(passwordEncoder().encode("123456"));

//        根据登陆账号查询用户对象
        daoAuthenticationProvider.setUserDetailsService(username -> {
//   此处user为自己的用户对象 非spring security user
            User user = userService.loadUserByUsername(username);
            if (ObjectUtils.isEmpty(user))
//         说明登陆账号不存在..... 没有查到
                throw new UsernameNotFoundException("USER_NOT_FOUND");
            else if (user.getLocked() == 1)
                throw new LockedException("USER_LOCKED");
//     else
//         throw new RuntimeException("其他未知异常");


//       交由spring security 进行后续密码认证
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            return new org.springframework.security.core.userdetails.User(username, user.getPass(), authorities);

        });

        //注入密码加密器
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        //        显示找不到用户异常
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
        return daoAuthenticationProvider;

    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
//前后端分离开发
//session cookie ？

//   csrf 跨站点伪造攻击 cookie server

//      1、  启用跨域访问
        http.cors();


//       2、 禁用csrf
        http.csrf().disable();

//       先创建会话 session
/**
 * 服务器端会话管理机制
 * SessionCreationPolicy.ALWAYS,  一直创建会话
 * SessionCreationPolicy.NEVER,   不主动创建会话，但有session也会使用
 * SessionCreationPolicy.IF_REQUIRED, 仅在需要Session时才会创建
 * SessionCreationPolicy.STATELESS;  不主动创建session，也不会使用session
 */

        http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

/**
 * 对于前端请求认证
 */
        http.authorizeRequests()
//           定义可以匿名访问的资源路径   druid开头的都放行
//            HttpMethod.GET 请求方式       "/druid/**"资源路径
//            只有druid开头可以访问 其他方法拦截下来
                .antMatchers(HttpMethod.GET, "/druid/**").permitAll()

//            所有请求必须认证后才会放行
                .anyRequest().authenticated();
/**
 * 异常处理
 */
        http.exceptionHandling()
//         403未认证异常                  请求    响应        认证异常
                .authenticationEntryPoint((request, response, Exception) -> {

                    ResultCode code = ResultCode.LOGIN_NOT_LOGGED;
                    ResultData data = ResultData.failure(code);
                    Result.result(response, data, HttpStatus.FORBIDDEN.value());
                });


        /**
         * 退出
         */
        http.logout()
//                                     请求       响应      认证对象
                .logoutSuccessHandler((request, response, authentication) -> {
//               通过请求头获取令牌
                  String accessToken=  request.getHeader("access-token");
//               从Redis中删除令牌
                    if (redisComponent.exists(accessToken))
//
                        redisComponent.del(accessToken);

//                    返回服务器响应
                    ResultCode code=ResultCode.LOGOUT_SUCCESS;
                    ResultData data=ResultData.success(code);
                    Result.result(response,data);

                });

//   默认 通过浏览器发送地址

        /**
         * 重写登陆认证过滤器
         * uri：login.do
         * METHOD:POST
         * new //   获取认证管理器  并把认证管理器传入进来
         */
        http.addFilter(new UsernamePasswordAuthenticationFilter(authenticationManager()) {

            /**
             * 默认收集浏览器发出的提交表单请求获取表单<username>与<password/>参数
             * 前后端分离开发收集来自与前端JSON（application/json）格式参数
             * @param request  请求
             * @param response  响应
             * @return 认证对象-》SecurityContext
             * @throws AuthenticationException 认证异常
             */
            @SneakyThrows // 异常注解
            @Override
            public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//               提交表单参数获取方式
//                String username = request.getParameter("username");
//                String password = request.getParameter("password");


//   1、 收集前端JSON格式参数    从流里面解析json  获取输入流 @RequestBody
//              读取输入流 获取 解析出前端json字符串封装到map集合中
                Map<String, String> params = objectMapper.readValue(request.getInputStream(), Map.class);


//SpringSecurity->SecurityContext 返回认证对象  存到SecurityContext

//    2、  创建spring security认证对象
                UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                        params.get("username"),
                        params.get("password")
                );

//   3、把创建好的对象写入Security Context中
                return authenticationManager().authenticate(token);
            }

            /**
             * 认证失败回调函数
             * @param request 请求
             * @param response 响应
             * @param failed 认证异常
             * @throws IOException IO 异常
             * @throws ServletException Service 异常
             */
            @Override
            protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
//   org.springframework.security.core.userdetails.UsernameNotFoundException: 账号不存在哦 嘻嘻
//   org.springframework.security.authentication.InternalAuthenticationServiceException: 账号已锁定
//   org.springframework.security.authentication.BadCredentialsException: 用户名或密码错误
//                 统一封装错误状态码及错误提示消息
                ResultCode code = ResultCode.LOGIN_OTHER_ERROR;
//               1、未找到用户名异常
                if (failed instanceof UsernameNotFoundException)
                    code = ResultCode.LOGIN_USER_NOT_EXIST;
                    //              2、账号已锁定异常
                else if (failed instanceof InternalAuthenticationServiceException && failed.getMessage().equals("user_locked"))
                    code = ResultCode.LOGIN_USER_LOCKED;
//               3、密码输出错误异常
                else if (failed instanceof BadCredentialsException)
                    code = ResultCode.LOGIN_USER_PASS_ERROR;
//              把封装过的状态转为JSON返回到客户端
//                response.setContentType("application/json;charset=UTF-8");
//                response.getWriter();

//               返回统一封装结果集
                ResultData data = ResultData.failure(code);
                Result.result(response, data);
            }

            /**
             * 认证成功回调函数
             * @param request 请求
             * @param response 响应
             * @param chain  过滤器链
             * @param authentication 认证成功对象
             * @throws IOException IO 异常
             * @throws ServletException  servlet 异常
             */
            @Override
            protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {

//          1、前后端分离 session cookie
//          1.1、android ios 不支持cookie
//          1.2、 session 不支持分布式应用
//          前后端分离  没有session cookie 解决方案
//          1、JWT：json web token 令牌：用户标示 过期标示 加密
//            优点：轻量级 服务器压力减小  缺点：只读 黑名单——》发出请求
//          2、Redis（充当session服务器端会话）  uuid（充当客户端cookie）
//                生成一个uuid存储到redis 并且把uuid返回到客户端


//          生成一块客户端访问服务器授权的令牌
                String accessToken = UUID.randomUUID().toString();
//     获取当前认证通过的用户属性对象
                org.springframework.security.core.userdetails.User user =
                        (org.springframework.security.core.userdetails.User) authentication.getPrincipal();
                String username = user.getUsername();

//        登陆时间  过期时间  活跃用户

//                获取登陆时间
                long loginTime = System.currentTimeMillis();
//              根据登陆账号获取用户权限集合
                List<String> codeList = userService.queryCodeByUsername(username);


// key?
                redisComponent.hmset(accessToken, Map.of("username", username,
                        "login_time", loginTime,
                        "code_list", codeList), 60 * 15);


                //              获取登陆成功标示
                ResultCode code = ResultCode.LOGIN_SUCCESS;

//             创建响应对象标示
                ResultData data = ResultData.success(code, accessToken);

//               返回JSON响应到前端
                Result.result(response, data);
            }


        });


        /**
         * 重写权限过滤器
         */
        http.addFilter(new BasicAuthenticationFilter(authenticationManager()) {

            /**
             * 拦截客户端除登陆外所有请求
             * @param request 请求
             * @param response 响应
             * @param chain 过滤器莲
             * @throws IOException io异常
             * @throws ServletException
             */
            @Override
            protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//
//            Enumeration<String> headers=  request.getHeaderNames();
//            while(headers.hasMoreElements()){
//
//                String header = headers.nextElement();
//                System.out.println(header+":"+request.getHeader(header));
//            }

//             从请求头中获取访问令牌
                String accessToken = request.getHeader("access-token");
//           客户端携带令牌在有效期内，允许客户端继续向下执行
                if (StringUtils.hasLength(accessToken) && redisComponent.exists(accessToken)) {


// 通过令牌token获取redis中数据
                    Map<String, Object> tokenObject = redisComponent.hmget(accessToken);
                    String username = (String) tokenObject.get("user_name");
// 通过令牌获取用户权限集合
                    List<String> codeList = (List<String>) tokenObject.get("code_list");

//                封装权限集合
                    List<GrantedAuthority> authorities = new ArrayList<>();
                    codeList.forEach(code -> {
                        authorities.add(new SimpleGrantedAuthority(code));
                    });
//                  方案1：  延长令牌过期时长
                    redisComponent.expire(accessToken,15*60);

//                    方案2：
//                    访问令牌：access_token
//                    刷新令牌：refresh_token et

                    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities);
//   通知spring Security 该请求已通过验证
                    SecurityContextHolder.getContext().setAuthentication(token);

                }

//            无论是否通过认证一律放行
                chain.doFilter(request, response);
            }

        });

    }
}
